package com.luych.toolbox.data.service.service;

import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.SftpException;
import com.luych.toolbox.common.enums.DataBackupHistoryStatus;
import com.luych.toolbox.common.enums.DataBackupHistoryType;
import com.luych.toolbox.common.feign.intf.base.service.UserFeign;
import com.luych.toolbox.common.feign.param.data.service.DataBackupHistoryParam;
import com.luych.toolbox.common.feign.view.data.service.DataAgentView;
import com.luych.toolbox.common.feign.view.data.service.DataBackupHistoryView;
import com.luych.toolbox.common.feign.view.data.service.DataBackupPlanView;
import com.luych.toolbox.common.feign.view.data.service.DataSourceView;
import com.luych.toolbox.common.page.PageParam;
import com.luych.toolbox.common.page.PageResult;
import com.luych.toolbox.common.service.base.BaseConvert;
import com.luych.toolbox.common.service.base.BaseService;
import com.luych.toolbox.common.service.exception.HttpException;
import com.luych.toolbox.common.service.page.SpringPageParamAdapter;
import com.luych.toolbox.common.service.page.SpringPageResultAdapter;
import com.luych.toolbox.common.service.tool.Cmd;
import com.luych.toolbox.data.service.constant.Constant;
import com.luych.toolbox.data.service.dao.*;
import com.luych.toolbox.data.service.entity.DataAgent;
import com.luych.toolbox.data.service.entity.DataBackupHistory;
import com.luych.toolbox.data.service.entity.DataBackupPlan;
import com.luych.toolbox.data.service.entity.DataSource;
import com.luych.toolbox.data.service.job.DataBackupHistoryExecuteJob;
import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.data.domain.Page;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;

@Service
public class DataBackupHistoryService
        extends BaseService<DataBackupHistoryView, DataBackupHistoryParam, DataBackupHistory>
        implements ApplicationListener<ApplicationReadyEvent> {
    private static final Logger LOGGER = LoggerFactory.getLogger(DataBackupHistoryService.class);

    private static final BaseConvert<DataBackupHistoryParam, DataBackupHistory> dataBackupHistoryParamToEntity = DataBackupHistory::new;
    private static final BaseConvert<DataBackupHistory, DataBackupHistoryView> dataBackupHistoryEntityToView = DataBackupHistoryView::new;
    private static final BaseConvert<DataBackupPlan, DataBackupPlanView> dataBackupPlanEntityToView = DataBackupPlanView::new;
    private static final BaseConvert<DataSource, DataSourceView> dataSourceEntityToView = DataSourceView::new;
    private static final BaseConvert<DataAgent, DataAgentView> dataAgentEntityToView = DataAgentView::new;

    private final Boolean rescheduleWhenStart;

    private final Scheduler scheduler;

    private final DataBackupPlanDao dataBackupPlanDao;
    private final DataBackupHistoryDao dataBackupHistoryDao;
    private final DataRecoveryHistoryDao dataRecoveryHistoryDao;
    private final DataSourceDao dataSourceDao;
    private final DataAgentDao dataAgentDao;

    private final UserFeign userFeign;

    @Autowired
    public DataBackupHistoryService(@Value("${spring.quartz.rescheduleWhenStart:false}") Boolean rescheduleWhenStart,
                                    Scheduler scheduler,
                                    DataBackupPlanDao dataBackupPlanDao,
                                    DataBackupHistoryDao dataBackupHistoryDao,
                                    DataRecoveryHistoryDao dataRecoveryHistoryDao,
                                    DataSourceDao dataSourceDao,
                                    DataAgentDao dataAgentDao,
                                    UserFeign userFeign) {
        super(dataBackupHistoryDao, dataBackupHistoryParamToEntity, dataBackupHistoryEntityToView);
        this.rescheduleWhenStart = rescheduleWhenStart;
        this.scheduler = scheduler;
        this.dataBackupPlanDao = dataBackupPlanDao;
        this.dataBackupHistoryDao = dataBackupHistoryDao;
        this.dataRecoveryHistoryDao = dataRecoveryHistoryDao;
        this.dataSourceDao = dataSourceDao;
        this.dataAgentDao = dataAgentDao;
        this.userFeign = userFeign;
    }

    @Override
    protected DataBackupHistoryView fill(DataBackupHistoryView view) {
        Optional<DataBackupHistory> dataBackupHistoryOpt = dataBackupHistoryDao.findById(view.getId());
        dataBackupHistoryOpt.map(DataBackupHistory::getDataSourceId).flatMap(dataSourceDao::findById).map(dataSourceEntityToView).ifPresent(view::setDataSource);
        dataBackupHistoryOpt.map(DataBackupHistory::getDataAgentId).flatMap(dataAgentDao::findById).map(dataAgentEntityToView).ifPresent(view::setDataAgent);
        dataBackupHistoryOpt.map(DataBackupHistory::getDataBackupPlanId).flatMap(dataBackupPlanDao::findById).map(dataBackupPlanEntityToView).ifPresent(view::setDataBackupPlan);
        dataBackupHistoryOpt.map(DataBackupHistory::getOwnerId).map(userFeign::get).ifPresent(view::setOwner);
        return super.fill(view);
    }

    private void setStatus(String id, DataBackupHistoryStatus status) {
        DataBackupHistory dataBackupHistory = dataBackupHistoryDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_BACKUP_PLAN_NOT_FOUND));
        dataBackupHistory.setStatus(status);
        dataBackupHistoryDao.save(dataBackupHistory);
    }

    private JobKey getJobKey(String id) {
        return new JobKey(id, DataBackupHistoryExecuteJob.class.getName());
    }

    private JobDetail getJobDetail(String id) {
        Map<String, Object> jobParam = new HashMap<String, Object>() {{
            this.put("id", id);
        }};
        return JobBuilder.newJob(DataBackupHistoryExecuteJob.class)
                .withIdentity(this.getJobKey(id))
                .usingJobData(new JobDataMap(jobParam))
                .build();
    }

    private Trigger getJobTrigger(String id) {
        return TriggerBuilder.newTrigger()
                .withIdentity(id, DataBackupHistoryExecuteJob.class.getName())
                .withSchedule(SimpleScheduleBuilder.repeatSecondlyForTotalCount(1, 1))
                .build();
    }

    @Override
    public DataBackupHistoryView create(DataBackupHistoryParam param) {
        Assert.notNull(param.getDataSourceId(), "Param dataSourceId can't be empty");
        Assert.notNull(param.getDataAgentId(), "Param dataAgentId can't be empty");
        Assert.notNull(param.getName(), "Param name can't be empty");
        Assert.notNull(param.getFile(), "Param file can't be empty");
        Assert.notNull(param.getOwnerId(), "Param ownerId can't be empty");

        // check file
        Optional<DataBackupHistory> dataBackupHistoryOpt = dataBackupHistoryDao.findByDataAgentIdAndFile(param.getDataAgentId(), param.getFile());
        if (dataBackupHistoryOpt.isPresent()) {
            throw new HttpException(HttpStatus.CONFLICT, String.format(Constant.DATA_BACKUP_HISTORY_SAVE_FILE_USED, param.getFile()));
        }

        // save
        DataBackupHistory dataBackupHistoryParam = dataBackupHistoryParamToEntity.apply(param);
        dataBackupHistoryParam.setType(DataBackupHistoryType.MANUAL);
        dataBackupHistoryParam.setStatus(DataBackupHistoryStatus.LOADING);
        DataBackupHistory dataBackupHistoryResult = dataBackupHistoryDao.save(dataBackupHistoryParam);
        DataBackupHistoryView view = this.fill(dataBackupHistoryEntityToView.apply(dataBackupHistoryResult));

        // backup
        try {
            scheduler.scheduleJob(this.getJobDetail(view.getId()), this.getJobTrigger(view.getId()));
        } catch (SchedulerException e) {
            throw new IllegalStateException(e);
        }

        return view;
    }

    @Override
    public DataBackupHistoryView modify(DataBackupHistoryParam param) {
        throw new HttpException(HttpStatus.METHOD_NOT_ALLOWED, Constant.DATA_BACKUP_HISTORY_MODIFY_REJECT);
    }

    @Override
    public void remove(String id) {
        if (dataRecoveryHistoryDao.countByDataBackupHistoryId(id) > 0) {
            throw new HttpException(HttpStatus.FORBIDDEN, Constant.DATA_BACKUP_HISTORY_DELETE_REJECT_USED);
        }

        DataBackupHistory backupHistory = dataBackupHistoryDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_BACKUP_HISTORY_NOT_FOUND));
        DataSource dataSource = dataSourceDao.findById(backupHistory.getDataSourceId()).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_SOURCE_NOT_FOUND));
        DataAgent dataAgent = dataAgentDao.findById(backupHistory.getDataAgentId()).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_AGENT_NOT_FOUND));
        String file = dataAgent.getPath() + "/" + backupHistory.getFile();
        String cmd = "rm -f " + file;
        try {
            Cmd.executeRemote(dataAgent.getHost(), dataAgent.getPort(), dataAgent.getUser(), dataAgent.getPass(), cmd);
        } catch (JSchException | IOException | InterruptedException e) {
            throw new IllegalStateException(e);
        }
        try {
            scheduler.deleteJob(this.getJobKey(id));
        } catch (SchedulerException e) {
            throw new IllegalStateException(e);
        }
        dataBackupHistoryDao.deleteById(id);
    }

    public ResponseEntity<Resource> download(String id) throws SftpException, JSchException, IOException {
        DataBackupHistory backupHistory = dataBackupHistoryDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_BACKUP_HISTORY_NOT_FOUND));
        DataAgent dataAgent = dataAgentDao.findById(backupHistory.getDataAgentId()).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_AGENT_NOT_FOUND));
        String file = dataAgent.getPath() + "/" + backupHistory.getFile();
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        Cmd.downloadRemote(dataAgent.getHost(), dataAgent.getPort(), dataAgent.getUser(), dataAgent.getPass(), file, outputStream);
        ByteArrayResource resource = new ByteArrayResource(outputStream.toByteArray());
        ContentDisposition disposition = ContentDisposition.builder("attachment")
                .filename(backupHistory.getFile(), StandardCharsets.UTF_8).build();
        return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .header(HttpHeaders.CONTENT_DISPOSITION, disposition.toString())
                .body(resource);
    }

    public PageResult<DataBackupHistoryView> pageByKeyword(PageParam pageParam, String keyword) {
        Page<DataBackupHistory> page;
        if (StringUtils.isEmpty(keyword)) {
            page = dataBackupHistoryDao.findAll(new SpringPageParamAdapter(pageParam));
        } else {
            page = dataBackupHistoryDao.findByNameLike("%" + keyword + "%", new SpringPageParamAdapter(pageParam));
        }
        PageResult<DataBackupHistory> pageResult = new SpringPageResultAdapter<>(page);
        return pageResult.convert(dataBackupHistoryEntityToView).convert(this::fill);
    }

    public PageResult<DataBackupHistoryView> pageByDataSource(PageParam pageParam, String dataSourceId) {
        Page<DataBackupHistory> page;
        if (StringUtils.isEmpty(dataSourceId)) {
            page = dataBackupHistoryDao.findAll(new SpringPageParamAdapter(pageParam));
        } else {
            page = dataBackupHistoryDao.findByDataSourceId(dataSourceId, new SpringPageParamAdapter(pageParam));
        }
        PageResult<DataBackupHistory> pageResult = new SpringPageResultAdapter<>(page);
        return pageResult.convert(dataBackupHistoryEntityToView).convert(this::fill);
    }

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        if (Boolean.TRUE.equals(rescheduleWhenStart)) {
            List<DataBackupHistoryStatus> statuses = Arrays.asList(DataBackupHistoryStatus.LOADING, DataBackupHistoryStatus.RUNNING);
            List<DataBackupHistory> dataBackupHistorys = dataBackupHistoryDao.findByStatusIn(statuses);
            for (DataBackupHistory dataBackupHistory : dataBackupHistorys) {
                try {
                    scheduler.deleteJob(this.getJobKey(dataBackupHistory.getId()));
                    scheduler.scheduleJob(this.getJobDetail(dataBackupHistory.getId()), this.getJobTrigger(dataBackupHistory.getId()));
                    this.setStatus(dataBackupHistory.getId(), DataBackupHistoryStatus.RUNNING);
                } catch (SchedulerException e) {
                    this.setStatus(dataBackupHistory.getId(), DataBackupHistoryStatus.EXCEPTION);
                    throw new IllegalStateException(e);
                }
            }
        }
    }
}
